From 458e6f2ee255dddf830a4da7b31b2b16a8332769 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Mon, 30 Nov 2020 21:41:14 -0600 Subject: [PATCH] Add home_page and menu_page concepts --- README.rst | 60 ++-- setup.py | 1 + src/pgwui_common/checkset.py | 6 +- src/pgwui_common/exceptions.py | 98 ++++++ src/pgwui_common/pgwui_common.py | 108 +++++-- src/pgwui_common/templates/auth_base.mak | 4 +- src/pgwui_common/templates/base.mak | 8 +- src/pgwui_common/views/__init__.py | 0 src/pgwui_common/views/ex_views.py | 36 +++ src/pgwui_common/views/page_views.py | 55 ++++ tests/test_pgwui_common.py | 360 +++++++++++++++++------ tests/views/test_ex_views.py | 52 ++++ tests/views/test_page_views.py | 107 +++++++ 13 files changed, 751 insertions(+), 144 deletions(-) create mode 100644 src/pgwui_common/views/__init__.py create mode 100644 src/pgwui_common/views/ex_views.py create mode 100644 src/pgwui_common/views/page_views.py create mode 100644 tests/views/test_ex_views.py create mode 100644 tests/views/test_page_views.py diff --git a/README.rst b/README.rst index 589842b..03b2406 100644 --- a/README.rst +++ b/README.rst @@ -58,24 +58,35 @@ experience with the Mako HTML templating system and/or the Python programming language. +Configuration Settings Common to All PGWUI Components +----------------------------------------------------- + +PGWUI components all have the following configuration settings: + + menu_label + The label for PGWUI_Menu to display, when different from the default + +Note that the ``menu_label`` setting appears within the settings for +the component, not at top-level. + + Usage ----- +This section is of interest to application developers. + When utilizing PGWUI modules in your own `Pyramid`_ application, modules which require the PGWUI_Common module, PGWUI_Common must be -explicitly configured. This can be done with any of `Pyramid's -`_ configuration mechanisms, ``pyramid_includes = -pgwui_common`` in a ``.ini`` file or -``config.include('pgwui_common')`` in Python code. +explicitly configured. This can be done with any of +`Pyramid's`_ configuration mechanisms, -Configuration Settings Common to All PGWUI Components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -PGWUI modules all have the following configuration settings: +``pyramid_includes = pgwui_common`` in a ``.ini`` file or +``config.include('pgwui_common')`` in Python code. - menu_label - The label for PGWUI_Menu to display, when different from the default +Unless PGWUI_Server is used, PGWUI_Common (and PGWUI_Core) expect all +required settings exist, as well as those which have defaults +established by PGWUI_Server. Common Template Variables @@ -84,24 +95,28 @@ Common Template Variables The `@base_view` decorator, and it's decendents, makes the following variables available in templates: - pgwui:: A dict, containing the following keys: + pgwui + A dict, containing the following keys: - routes:: A dict, keyed by PGWUI component name. Each value is the - URL used to reach the component. There are the following special - component names: + urls + A dict, keyed by PGWUI component name. Each value is the + URL used to reach the component. There are the following special + component names: - pgwui_home:: The URL to the pgwui.home_page setting. This key - is always available. + pgwui_home + The URL to the pgwui.home_page setting. This key + is always available. - pgwui_menu:: The URL to the menu of PGWUI components. This - obtains its value from the ``pgwui.menu_page`` configuration - setting, if present. Otherwise it is the URL to the PGWUI_Menu - component, if the component is present. Otherwise the key - does not exist. + pgwui_menu + The URL to the menu of PGWUI components. This + obtains its value from the ``pgwui.menu_page`` configuration + setting, if present. Otherwise it is the URL to the PGWUI_Menu + component, if the component is present. Otherwise the key + does not exist. Configuration By Python Code ----------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you are writing a complete `Pyramid`_ application, or simply want full control over configuration you will need to make a Python @@ -161,3 +176,4 @@ provided by `The Dian Fossey Gorilla Fund .. _PostgreSQL: https://www.postgresql.org/ .. _Pyramid: https://trypyramid.com/ +.. _Pyramid's: `Pyramid`_ diff --git a/setup.py b/setup.py index 0afd2c9..0168cf7 100644 --- a/setup.py +++ b/setup.py @@ -145,6 +145,7 @@ setup( 'pyramid', 'pyramid_beaker', 'pyramid_mako', + 'attrs', ], # List additional groups of dependencies here (e.g. development diff --git a/src/pgwui_common/checkset.py b/src/pgwui_common/checkset.py index 679dca9..2be79ab 100644 --- a/src/pgwui_common/checkset.py +++ b/src/pgwui_common/checkset.py @@ -32,7 +32,7 @@ def require_settings(component, required_settings, conf): for setting in required_settings: if setting not in conf: errors.append(exceptions.MissingSettingError( - '{}.{}'.format(component, setting))) + '{}:{}'.format(component, setting))) return errors @@ -41,7 +41,7 @@ def unknown_settings(component, settings, conf): for setting in conf: if setting not in settings: errors.append(exceptions.UnknownSettingKeyError( - '{}.{}'.format(component, setting))) + '{}:{}'.format(component, setting))) return errors @@ -56,5 +56,5 @@ def boolean_settings(component, booleans, conf): if (val is not True and val is not False): errors.append(exceptions.NotBooleanSettingError( - '{}.{}'.format(component, setting), conf[setting])) + '{}:{}'.format(component, setting), conf[setting])) return errors diff --git a/src/pgwui_common/exceptions.py b/src/pgwui_common/exceptions.py index 95a5022..02122f0 100644 --- a/src/pgwui_common/exceptions.py +++ b/src/pgwui_common/exceptions.py @@ -42,8 +42,106 @@ class MissingSettingError(Error): super().__init__('Missing PGWUI setting: {}'.format(key)) +class BadPageTypeError(Error): + def __init__(self, key, val): + super().__init__(f'Bad {key} setting ({val})') + + +class BadPageSourceError(Error): + def __init__(self, msg): + super().__init__(msg) + + +class BadURLSourceError(BadPageSourceError): + def __init__(self, key, val): + super().__init__( + f'Bad {key} setting for the "URL" type, ({val}) ' + 'does not look like an URL') + + +class BadFileSourceError(BadPageSourceError): + def __init__(self, key, val): + super().__init__( + f'Bad {key} setting for a "file" type, ({val}) ' + 'does not look like a file system path beginning with "/"') + + +class BadFileURLPathError(BadPageSourceError): + def __init__(self, key, val): + super().__init__( + f'Bad {key} setting for a "file" type, ({val}) ' + 'does not look like a "path" component of an URL ' + 'that begins with "/"') + + +class BadRouteSourceError(BadPageSourceError): + def __init__(self, key, val): + super().__init__( + f'Bad {key} setting for a "route" type, ({val}) ' + 'does not look like a Pyramid route name') + + +class BadAssetSourceError(BadPageSourceError): + def __init__(self, key, val): + super().__init__( + f'Bad {key} setting for an "asset" type, ({val}) ' + 'does not look like a Pyramid asset specification') + + class NotBooleanSettingError(Error): def __init__(self, key, value): super().__init__( 'The "{}" PGWUI setting must be "True" or "False"' .format(key)) + + +class ViewError(Error): + pass + + +class BadPageError(ViewError): + def __init__(self, page, ex, msg): + self.page = page + self.ex = ex + super().__init__(msg) + + +class BadPageFileNotFoundError(BadPageError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + f'a file ({ex.filename}) that does not exist') + + +class BadPageFilePermissionError(BadPageError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + f'a file ({ex.filename}) which cannot be read due to file ' + 'system permissions') + + +class BadPageIsADirectoryError(BadPageError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + f'a directory ({ex.filename}), not a file') + + +class BadRouteError(BadPageError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + 'a route that does not exist') + + +class BadAssetError(BadPageError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + 'an asset that does not exist') diff --git a/src/pgwui_common/pgwui_common.py b/src/pgwui_common/pgwui_common.py index cba4c90..edbd819 100644 --- a/src/pgwui_common/pgwui_common.py +++ b/src/pgwui_common/pgwui_common.py @@ -19,50 +19,87 @@ # Karl O. Pinc -'''Provide a way to configure PGWUI. +'''Configure supporting modules and other common elements +View decorators to expose useful variables to templates ''' +import pgwui_common.views.page_views from .plugin import find_pgwui_components +from . import exceptions as ex -DEFAULT_HOME_ROUTE = '/' +def route_path(request, page_name, source): + '''Return the route path of the page's "source" + ''' + try: + return request.route_path(source) + except KeyError as old_ex: + raise ex.BadRouteError(page_name, old_ex) -def set_menu_route(request, routes): - '''Add routes for pgwui_menu, return non-menu components +def asset_path(request, page_name, source): + '''Return the static path to the asset's "source" + ''' + try: + return request.static_path(source) + except ValueError as old_ex: + raise ex.BadAssetError(page_name, old_ex) + + +def url_of_page(request, page_name): + '''Return a url to the page. This may or may not be fully + qualified, depending on what the user specifies in the settings. + ''' + page_conf = request.registry.settings['pgwui'][page_name] + type = page_conf['type'] + source = page_conf['source'] + if type == 'URL': + return source + if type == 'file': + return request.route_path(f'pgwui_common.{page_name}') + if type == 'route': + return route_path(request, page_name, source) + if type == 'asset': + return asset_path(request, page_name, source) + + +def set_menu_url(request, urls): + '''Add urls for pgwui_menu, return non-menu components ''' try: - menu_url = request.route_url('pgwui_menu') + menu_url = url_of_page(request, 'menu_page') except KeyError: - pass - else: - if menu_url != request.route_url('pgwui_home'): - routes['pgwui_menu'] = request.route_path('pgwui_menu') + try: + menu_url = request.route_path('pgwui_menu') + except KeyError: + return + if menu_url != urls['pgwui_home']: + urls['pgwui_menu'] = menu_url -def set_component_routes(request, routes): - '''Add routes for each pgwui component to the 'routes' dict +def set_component_urls(request, urls): + '''Add urls for each pgwui component to the 'urls' dict ''' - set_menu_route(request, routes) + set_menu_url(request, urls) components = find_pgwui_components() if 'pgwui_menu' in components: components.remove('pgwui_menu') for component in components: try: - route = request.route_path(component) + url = request.route_path(component) except KeyError: pass # In case a component has no route else: - routes.setdefault(component, route) + urls.setdefault(component, url) -def set_routes(request, routes): - '''Build 'routes' dict with all the routes +def set_urls(request, urls): + '''Build 'urls' dict with all the urls ''' - home_route = request.route_path('pgwui_home') - routes.setdefault('pgwui_home', home_route) - set_component_routes(request, routes) + home_url = url_of_page(request, 'home_page') + urls.setdefault('pgwui_home', home_url) + set_component_urls(request, urls) def base_view(wrapped): @@ -73,8 +110,8 @@ def base_view(wrapped): ''' response = wrapped(request) pgwui = response.get('pgwui', {}) - routes = pgwui.setdefault('routes', dict()) - set_routes(request, routes) + urls = pgwui.setdefault('urls', dict()) + set_urls(request, urls) response['pgwui'] = pgwui return response return wrapper @@ -90,6 +127,32 @@ def auth_base_view(wrapped): return wrapper +def configure_page(config, pgwui_settings, page_name): + '''Setup route and view for a file given in pgwui."page_name" setting, + which is the name of the new route. + + Only files need anything done. URLs are used as written into the + config, and routes and assets already exist. + ''' + if page_name in pgwui_settings: + page_settings = pgwui_settings[page_name] + type = page_settings['type'] + if type == 'file': + route_name = f'pgwui_common.{page_name}' + with config.route_prefix_context(None): + config.add_route(route_name, page_settings['url_path']) + config.add_view(pgwui_common.views.page_views.PageViewer, + attr=page_name, route_name=route_name) + + +def configure_pages(config): + '''Setup routes and views for "pgwui.xxxx_page" settings + ''' + pgwui_settings = config.get_settings()['pgwui'] + configure_page(config, pgwui_settings, 'home_page') + configure_page(config, pgwui_settings, 'menu_page') + + def includeme(config): '''Pyramid configuration for PGWUI_Common ''' @@ -99,4 +162,5 @@ def includeme(config): 'static/pgwui_common', 'pgwui_common:static/', cache_max_age=3600) - config.add_route('pgwui_home', DEFAULT_HOME_ROUTE) + configure_pages(config) + config.scan() diff --git a/src/pgwui_common/templates/auth_base.mak b/src/pgwui_common/templates/auth_base.mak index 0837aba..243030e 100644 --- a/src/pgwui_common/templates/auth_base.mak +++ b/src/pgwui_common/templates/auth_base.mak @@ -52,8 +52,8 @@ <%def name="navbar_content()"> ${parent.navbar_content()} - % if 'pgwui_logout' in pgwui['routes']: - | Logout + % if 'pgwui_logout' in pgwui['urls']: + | Logout % endif diff --git a/src/pgwui_common/templates/base.mak b/src/pgwui_common/templates/base.mak index a66324c..51c4ee4 100644 --- a/src/pgwui_common/templates/base.mak +++ b/src/pgwui_common/templates/base.mak @@ -26,14 +26,14 @@ This template uses the following variables in it's context: pgwui Dict - routes Dict of routes, keyed by pgwui component name + urls Dict of urls, keyed by pgwui component name <%def name="navbar_content()"> - HOME - % if 'pgwui_menu' in pgwui['routes']: - | Menu + HOME + % if 'pgwui_menu' in pgwui['urls']: + | Menu % endif diff --git a/src/pgwui_common/views/__init__.py b/src/pgwui_common/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pgwui_common/views/ex_views.py b/src/pgwui_common/views/ex_views.py new file mode 100644 index 0000000..a7ecfc6 --- /dev/null +++ b/src/pgwui_common/views/ex_views.py @@ -0,0 +1,36 @@ +# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +'''Views for exceptions that are raised +''' + +from pyramid.view import exception_view_config + +from pgwui_common import exceptions as ex + +NOT_FOUND = '404 Not Found' + + +@exception_view_config(ex.BadPageError, renderer='string') +def bad_config_view(ex, request): + request.response.status_code = 404 + request.response.status = NOT_FOUND + return f'PGWUI Configuration Error:\n{ex}:\n{ex.ex}' diff --git a/src/pgwui_common/views/page_views.py b/src/pgwui_common/views/page_views.py new file mode 100644 index 0000000..956c17c --- /dev/null +++ b/src/pgwui_common/views/page_views.py @@ -0,0 +1,55 @@ +# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +'''Return a page that's a file in the file system +''' + +import attr +from pyramid.response import FileResponse + +from pgwui_common import exceptions as ex + + +@attr.s +class PageViewer(): + '''A class of views that return file content + ''' + request = attr.ib() + + def page(self, page_name): + try: + return FileResponse( + self.request.registry.settings['pgwui'][page_name]['source'], + request=self.request, + content_type='text/html', + cache_max_age=3600) + except FileNotFoundError as old_ex: + raise ex.BadPageFileNotFoundError(page_name, old_ex) + except PermissionError as old_ex: + raise ex.BadPageFilePermissionError(page_name, old_ex) + except IsADirectoryError as old_ex: + raise ex.BadPageIsADirectoryError(page_name, old_ex) + + def menu_page(self): + return self.page('menu_page') + + def home_page(self): + return self.page('home_page') diff --git a/tests/test_pgwui_common.py b/tests/test_pgwui_common.py index d735ee0..ab95852 100644 --- a/tests/test_pgwui_common.py +++ b/tests/test_pgwui_common.py @@ -21,9 +21,11 @@ import pytest import pyramid.config +import pyramid.testing from pyramid.threadlocal import get_current_request import pgwui_common.pgwui_common as pgwui_common +import pgwui_common.exceptions as common_ex from pgwui_testing import testing @@ -38,11 +40,19 @@ FOO_URL = 'foo://bar/' mock_find_pgwui_components = testing.make_mock_fixture( pgwui_common, 'find_pgwui_components') -mock_route_path = testing.instance_method_mock_fixture('route_path') +mock_method_route_path = testing.instance_method_mock_fixture('route_path') mock_route_url = testing.instance_method_mock_fixture('route_url') +mock_include = testing.instance_method_mock_fixture('include') +mock_add_static_view = testing.instance_method_mock_fixture('add_static_view') +mock_add_route = testing.instance_method_mock_fixture('add_route') +mock_add_view = testing.instance_method_mock_fixture('add_view') +mock_static_path = testing.instance_method_mock_fixture('static_path') def mock_view(request): + if (hasattr(request, 'registry') + and 'pgwui' in request.registry.settings): + return request.registry.settings return {'pgwui': {'foo': FOO_URL}} @@ -52,129 +62,249 @@ def check_base_view_results(request, pgwui): # Unit tests -# set_menu_route() +# route_path() +@pytest.mark.unittest +def test_route_path_with_path(pyramid_request_config, mock_method_route_path): + '''static_path() result is returned + ''' + expected = 'route' + + request = get_current_request() + mocked_route_path = mock_method_route_path(request) + mocked_route_path.return_value = expected + + result = pgwui_common.route_path(request, None, None) + + assert result == expected + + +@pytest.mark.unittest +def test_route_path_no_path(pyramid_request_config, mock_method_route_path): + '''BadRouteError() raised when there's no path + ''' + request = get_current_request() + mocked_route_path = mock_method_route_path(request) + mocked_route_path.side_effect = KeyError + + with pytest.raises(common_ex.BadRouteError): + pgwui_common.route_path(request, None, None) + + assert True + + +mock_route_path = testing.make_mock_fixture( + pgwui_common, 'route_path') + + +# asset_path() + +@pytest.mark.unittest +def test_asset_path_with_path(pyramid_request_config, mock_static_path): + '''static_path() result is returned + ''' + expected = 'static' + + request = get_current_request() + mocked_static_path = mock_static_path(request) + mocked_static_path.return_value = expected + + result = pgwui_common.asset_path(request, None, None) + + assert result == expected + + +@pytest.mark.unittest +def test_asset_path_no_path(pyramid_request_config, mock_static_path): + '''BadAssetError() raised when there's no path + ''' + request = get_current_request() + mocked_static_path = mock_static_path(request) + mocked_static_path.side_effect = ValueError + + with pytest.raises(common_ex.BadAssetError): + pgwui_common.asset_path(request, None, None) + + assert True + + +mock_asset_path = testing.make_mock_fixture( + pgwui_common, 'asset_path') + + +# url_of_page() + +@pytest.mark.parametrize( + ('pgwui', 'page_name', 'expected'), [ + ({'test_page': {'type': 'URL', + 'source': 'somesource'}}, + 'test_page', + 'somesource'), + ({'test_page': {'type': 'file', + 'source': 'somesource'}}, + 'test_page', + 'pgwui_common.test_page'), + ({'test_page': {'type': 'route', + 'source': 'somesource'}}, + 'test_page', + 'routepath'), + ({'test_page': {'type': 'asset', + 'source': 'somesource'}}, + 'test_page', + 'static'), + ({'test_page': {'type': 'impossible', + 'source': 'somesource'}}, + 'test_page', + None)]) +@pytest.mark.unittest +def test_url_of_page( + pyramid_request_config, mock_method_route_path, + mock_route_path, mock_asset_path, pgwui, page_name, expected): + '''The right results and calls are made + ''' + mock_asset_path.return_value = 'static' + mock_route_path.return_value = 'routepath' + + request = get_current_request() + mocked_route_path = mock_method_route_path(request) + mocked_route_path.side_effect = lambda x: x + + request.registry.settings['pgwui'] = pgwui + result = pgwui_common.url_of_page(request, page_name) + + assert result == expected + + +mock_url_of_page = testing.make_mock_fixture( + pgwui_common, 'url_of_page') + + +# set_menu_url() @pytest.mark.unittest @pytest.mark.parametrize( - "test_routes,expected", + "test_urls,expected", [ - # menu and home have identical routes, no route is added for menu + # menu and home have identical urls, no url is added for menu ({'pgwui_menu': '/', 'pgwui_home': '/'}, {}), - # No menu route, no route is added for menu + # No menu url, no url is added for menu ({'pgwui_home': '/'}, {}), - # menu and home have different urls, route is added for menu + # menu and home have different urls, url is added for menu ({'pgwui_menu': '/menu', 'pgwui_home': '/'}, {'pgwui_menu': '/menu'})]) -def test_set_menu_route( - pyramid_request_config, mock_route_path, mock_route_url, - test_routes, expected): - '''The expected routes are returned +def test_set_menu_url( + pyramid_request_config, mock_method_route_path, mock_url_of_page, + test_urls, expected): + '''The expected urls are returned ''' def path_func(name): - return test_routes[name] - - def url_func(name): - return f'{request.application_url}{test_routes[name]}' + return test_urls[name] + mock_url_of_page.side_effect = lambda *args: test_urls['pgwui_menu'] request = get_current_request() - mocked_route_path = mock_route_path(request) + mocked_route_path = mock_method_route_path(request) mocked_route_path.side_effect = path_func - mocked_route_url = mock_route_url(request) - mocked_route_url.side_effect = url_func - routes = dict() - pgwui_common.set_menu_route(request, routes) + urls = {'pgwui_home': test_urls['pgwui_home']} + expected.update(urls) + pgwui_common.set_menu_url(request, urls) - assert routes == expected + assert urls == expected -mock_set_menu_route = testing.make_mock_fixture( - pgwui_common, 'set_menu_route') +mock_set_menu_url = testing.make_mock_fixture( + pgwui_common, 'set_menu_url') -# set_component_routes() +# set_component_urls() +@pytest.mark.parametrize( + 'test_urls', [ + # With a pgwui_menu + {'pgwui_menu': '/menu', + 'pgwui_logout': '/logout', + 'pgwui_foo': '/foo', + 'pgwui_home': '/'}, + # Without a pgwui_menu + {'pgwui_logout': '/logout', + 'pgwui_foo': '/foo', + 'pgwui_home': '/'}]) @pytest.mark.unittest -def test_set_component_routes( - pyramid_request_config, mock_route_path, mock_set_menu_route, - mock_find_pgwui_components): - '''Routes are set for every component which has a route, except for +def test_set_component_urls( + pyramid_request_config, mock_method_route_path, mock_set_menu_url, + mock_find_pgwui_components, test_urls): + '''Urls are set for every component which has a route, except for pgwui_menu ''' - test_routes = {'pgwui_menu': '/menu', - 'pgwui_logout': '/logout', - 'pgwui_foo': '/foo', - 'pgwui_home': '/'} - test_components = list(test_routes) + ['pgwui_noroute'] + test_components = list(test_urls) + ['pgwui_noroute'] - def route_func(route): - return test_routes[route] + def url_func(url): + return test_urls[url] request = get_current_request() - mocked_route_path = mock_route_path(request) - mocked_route_path.side_effect = route_func + mocked_route_path = mock_method_route_path(request) + mocked_route_path.side_effect = url_func mock_find_pgwui_components.return_value = test_components - routes = dict() - pgwui_common.set_component_routes(request, routes) + urls = dict() + pgwui_common.set_component_urls(request, urls) - expected_routes = test_routes.copy() - del expected_routes['pgwui_menu'] + expected_urls = test_urls.copy() + if 'pgwui_menu' in expected_urls: + del expected_urls['pgwui_menu'] - assert routes == expected_routes + mock_set_menu_url.assert_called_once() + assert urls == expected_urls -mock_set_component_routes = testing.make_mock_fixture( - pgwui_common, 'set_component_routes') +mock_set_component_urls = testing.make_mock_fixture( + pgwui_common, 'set_component_urls') -# set_routes() +# set_urls() @pytest.mark.unittest -def test_set_routes( - pyramid_request_config, mock_set_component_routes, mock_route_path): - '''The 'home' route is added and set_component_routes() called +def test_set_urls( + pyramid_request_config, mock_url_of_page, mock_set_component_urls): + '''The 'home' url is added and set_component_urls() called ''' + test_home_route = '/' request = get_current_request() - mocked_route_path = mock_route_path(request) - mocked_route_path.return_value = pgwui_common.DEFAULT_HOME_ROUTE + mock_url_of_page.return_value = test_home_route - routes = dict() - pgwui_common.set_routes(request, routes) + urls = dict() + pgwui_common.set_urls(request, urls) - assert routes['pgwui_home'] == pgwui_common.DEFAULT_HOME_ROUTE - mock_set_component_routes.assert_called_once() + assert urls['pgwui_home'] == test_home_route + mock_set_component_urls.assert_called_once() -mock_set_routes = testing.make_mock_fixture( - pgwui_common, 'set_routes') +mock_set_urls = testing.make_mock_fixture( + pgwui_common, 'set_urls') # base_view() @pytest.mark.unittest -def test_base_view_routes(pyramid_request_config, mock_set_routes): - '''The response has the 'pgwui['routes']' dict added to it''' +def test_base_view_urls(mock_set_urls): + '''The response has the 'pgwui['urls']' dict added to it''' def mock_view(request): return {} - pgwui_common.includeme(pyramid_request_config) wrapper = pgwui_common.base_view(mock_view) response = wrapper(get_current_request()) assert 'pgwui' in response pgwui = response['pgwui'] - assert 'routes' in pgwui - assert isinstance(pgwui['routes'], dict) + assert 'urls' in pgwui + assert isinstance(pgwui['urls'], dict) @pytest.mark.unittest -def test_base_view_default(pyramid_request_config): +def test_base_view_default(mock_set_urls): '''The response retains the mock view's variables''' - pgwui_common.includeme(pyramid_request_config) wrapper = pgwui_common.base_view(mock_view) request = get_current_request() response = wrapper(request) @@ -188,11 +318,9 @@ mock_base_view = testing.make_mock_fixture(pgwui_common, 'base_view') # auth_base_view() @pytest.mark.unittest -def test_auth_base_view(pyramid_request_config, mock_base_view): +def test_auth_base_view(mock_base_view): '''Wrapper calls base_view() ''' - pgwui_common.includeme(pyramid_request_config) - wrapper = pgwui_common.auth_base_view(mock_view) request = get_current_request() wrapper(request) @@ -200,32 +328,75 @@ def test_auth_base_view(pyramid_request_config, mock_base_view): mock_base_view.assert_called_once() -# includeme() +# configure_page() @pytest.mark.unittest -def test_includeme_configurecalled(): - '''Pyramid Configure() methods are called''' - class MockConfig(): - def __init__(self): - self.include_called = False - self.add_static_view_called = False - self.home_route = None +def test_configure_page_no_page(): + '''When there's no setting for the page, nothing is done + ''' + pgwui_common.configure_page(None, {}, 'test_page') - def include(self, *args): - self.include_called = True - def add_static_view(self, *args, **kwargs): - self.add_static_view_called = True +@pytest.mark.unittest +def test_configure_page_not_file(): + '''When the type of the page is not "file",nothing is done + ''' + pgwui_common.configure_page( + None, {'test_page': {'type': 'other'}}, 'test_page') - def add_route(self, name, route): - if name == 'pgwui_home': - self.home_route = route - config = MockConfig() - pgwui_common.includeme(config) - assert config.include_called - assert config.add_static_view_called - assert config.home_route == '/' +@pytest.mark.unittest +def test_configure_page_file( + pyramid_request_config, mock_add_route, mock_add_view): + '''When the type of the page is "file", a route and view are added + ''' + mocked_add_route = mock_add_route(pyramid_request_config) + mocked_add_view = mock_add_view(pyramid_request_config) + pgwui_common.configure_page( + pyramid_request_config, + {'test_page': {'type': 'file', 'url_path': 'somepath'}}, + 'test_page') + + mocked_add_route.assert_called_once() + mocked_add_view.assert_called_once() + + +mock_configure_page = testing.make_mock_fixture( + pgwui_common, 'configure_page') + + +# configure_pages() + +@pytest.mark.unittest +def test_configure_pages(pyramid_request_config, mock_configure_page): + '''Calls configure_page() with all the pages + ''' + pgwui = 'pgwui' + pyramid_request_config.get_settings()['pgwui'] = pgwui + pgwui_common.configure_pages(pyramid_request_config) + + assert (set([call[0] for call in mock_configure_page.call_args_list]) + == set([(pyramid_request_config, pgwui, 'home_page'), + (pyramid_request_config, pgwui, 'menu_page')])) + + +mock_configure_pages = testing.make_mock_fixture( + pgwui_common, 'configure_pages') + + +# includeme() + +@pytest.mark.unittest +def test_includeme_configurecalled( + mock_add_static_view, mock_include, mock_configure_pages): + '''Pyramid Configure() methods are called''' + with pyramid.testing.testConfig() as config: + mocked_include = mock_include(config) + mocked_add_static_view = mock_add_static_view(config) + pgwui_common.includeme(config) + assert mocked_include.call_count == 2 + mocked_add_static_view.assert_called_once() + mock_configure_pages.assert_called_once() # Integration tests @@ -235,29 +406,36 @@ def test_includeme_configurecalled(): @pytest.mark.integrationtest def test_auth_base_view_integration( pyramid_request_config, mock_find_pgwui_components): - '''There are routes for every component + '''There are urls for every component ''' - test_routes = { + test_pgwui = {'home_page': {'type': 'URL', 'source': '/'}} + + test_urls = { 'pgwui_menu': '/menu', 'pgwui_logout': '/logout', 'pgwui_foo': '/foo'} - mock_find_pgwui_components.return_value = list(test_routes) + mock_find_pgwui_components.return_value = list(test_urls) + pyramid_request_config.add_settings(pgwui=test_pgwui) pgwui_common.includeme(pyramid_request_config) - for name, route in test_routes.items(): - pyramid_request_config.add_route(name, route) + for name, url in test_urls.items(): + pyramid_request_config.add_route(name, url) wrapper = pgwui_common.auth_base_view(mock_view) request = get_current_request() result = wrapper(request) - assert result['pgwui']['routes'] == dict(test_routes, pgwui_home='/') + assert result['pgwui']['urls'] == dict(test_urls, pgwui_home='/') # includeme() @pytest.mark.integrationtest def test_includeme(): + pgwui = {'home_page': {'type': 'file', + 'url_path': '/'}} + config = pyramid.config.Configurator() + config.registry.settings['pgwui'] = pgwui pgwui_common.includeme(config) diff --git a/tests/views/test_ex_views.py b/tests/views/test_ex_views.py new file mode 100644 index 0000000..030073c --- /dev/null +++ b/tests/views/test_ex_views.py @@ -0,0 +1,52 @@ +# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +import pytest +from pyramid.threadlocal import get_current_request + +import pgwui_common.views.ex_views as ex_views +import pgwui_common.exceptions as common_ex + +import pgwui_testing.testing as testing + +# Activiate our pytest plugin +# pytest_plugins = ("pgwui",) + + +mock_exception_view_config = testing.make_mock_fixture( + ex_views, 'exception_view_config') + + +# Unit tests + +# bad_config_view() + +@pytest.mark.unittest +def test_bad_config_view(pyramid_request_config, mock_exception_view_config): + '''Modifies the request, returns an expected response + ''' + request = get_current_request() + result = ex_views.bad_config_view( + common_ex.BadPageError(None, None, None), request) + + assert isinstance(result, str) + assert request.response.status_code == 404 + assert request.response.status == ex_views.NOT_FOUND diff --git a/tests/views/test_page_views.py b/tests/views/test_page_views.py new file mode 100644 index 0000000..dcd0512 --- /dev/null +++ b/tests/views/test_page_views.py @@ -0,0 +1,107 @@ +# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +import pytest +from pyramid.threadlocal import get_current_request + +import pgwui_common.exceptions as common_ex +import pgwui_common.views.page_views as page_views + +from pgwui_testing import testing + +# Activiate our pytest plugin +# pytest_plugins = ("pgwui",) + + +# Helper functions and constants + +mock_page = testing.instance_method_mock_fixture('page') +mock_FileResponse = testing.make_mock_fixture( + page_views, 'FileResponse') + + +# Unit tests + +# PageViewer.home_page() + +@pytest.mark.unittest +def test_home_page(pyramid_request_config, mock_page): + '''Called with correct name + ''' + request = get_current_request() + view = page_views.PageViewer(request) + mocked_page = mock_page(view) + view.home_page() + assert mocked_page.call_args[0][0] == 'home_page' + + +# PageViewer.menu_page() + +@pytest.mark.unittest +def test_menu_page(pyramid_request_config, mock_page): + '''Called with correct name + ''' + request = get_current_request() + view = page_views.PageViewer(request) + mocked_page = mock_page(view) + view.menu_page() + assert mocked_page.call_args[0][0] == 'menu_page' + + +# PageViewer.page() + +@pytest.mark.unittest +def test_page_success(pyramid_request_config, mock_FileResponse): + '''FileResponse() called + ''' + expected = 'some value' + mock_FileResponse.return_value = expected + + pgwui = {'test_page': {'source': 'anything'}} + request = get_current_request() + request.registry.settings['pgwui'] = pgwui + view = page_views.PageViewer(request) + result = view.page('test_page') + + mock_FileResponse.assert_called_once() + assert result == expected + + +@pytest.mark.parametrize( + ('ex', 'expected'), [ + (FileNotFoundError, common_ex.BadPageFileNotFoundError), + (PermissionError, common_ex.BadPageFilePermissionError), + (IsADirectoryError, common_ex.BadPageIsADirectoryError)]) +@pytest.mark.unittest +def test_page_exception(pyramid_request_config, mock_FileResponse, + ex, expected): + '''The correct exception is raised + ''' + mock_FileResponse.side_effect = ex + + pgwui = {'test_page': {'source': 'anything'}} + request = get_current_request() + request.registry.settings['pgwui'] = pgwui + view = page_views.PageViewer(request) + with pytest.raises(expected): + view.page('test_page') + + mock_FileResponse.assert_called_once() -- 2.34.1